package com.yc.cloud.common.oss.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yc.cloud.common.basic.dto.BasePageReqDTO;
import com.yc.cloud.common.basic.exception.Asserts;
import com.yc.cloud.common.basic.utils.MyFileUtils;
import com.yc.cloud.common.basic.utils.PathUtils;
import com.yc.cloud.common.basic.utils.SnowflakeIdGenerator;
import com.yc.cloud.common.oss.common.OssErrorTips;
import com.yc.cloud.common.oss.comparator.ChunkFileComparator;
import com.yc.cloud.common.oss.comparator.OssFileListComparator;
import com.yc.cloud.common.oss.entity.OssFile;
import com.yc.cloud.common.oss.enums.OssCategoryEnums;
import com.yc.cloud.common.oss.mapper.OssFileMapper;
import com.yc.cloud.common.oss.request.*;
import com.yc.cloud.common.oss.service.OssFileService;
import com.yc.cloud.common.oss.utils.VideoUtils;
import com.yc.cloud.common.oss.vo.GetChunkVo;
import com.yc.cloud.common.oss.vo.OssFileVo;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 文件 服务实现类
 * </p>
 *
 * @author Yancy
 * @since 2023-12-25
 */
@Service
@Slf4j
public class OssFileServiceImpl extends ServiceImpl<OssFileMapper, OssFile> implements OssFileService {

    @Resource
    private OssFileMapper ossFileMapper;

    private SnowflakeIdGenerator snowflakeIdGenerator;

    private final String CHUNK_SUFFIX = ".chunk";

    private final PathUtils pathUtils;

    // 设置目标尺寸为1024x768,后面开放可配置
    private final int convertWidth = 1024;
    private final int convertHeight = 768;

    public OssFileServiceImpl(PathUtils pathUtils) {
        this.pathUtils = pathUtils;
    }

    @PostConstruct
    public void init() {
        snowflakeIdGenerator = new SnowflakeIdGenerator(1L);
    }

    @Override
    public IPage<OssFileVo> list(OssListParam param, BasePageReqDTO queryParam) {
        val query = new QueryWrapper<OssFile>();
        IPage<OssFile> page;
        if (param.getParentFileId() != null) {
            query.eq(OssFile.PARENT_FILE_ID, param.getParentFileId());
        }
        if (param.getCompanyId() != null) {
            query.eq(OssFile.COMPANY_ID, param.getCompanyId());
        }
        if (param.getMemberId() != null) {
            query.eq(OssFile.CREATE_BY, param.getMemberId());
        }
        if (CollUtil.isNotEmpty(param.getMemberIds())) {
            query.in(OssFile.CREATE_BY, param.getMemberIds());
        }
        if (StrUtil.isNotEmpty(param.getSuffix())) {
            query.in(OssFile.FILE_EXTENSION, Arrays.asList(param.getSuffix().split(",")));
        }
        if (StrUtil.isNotEmpty(param.getManufacturer())) {
            query.in(OssFile.MANUFACTURER, Arrays.asList(param.getManufacturer().split(",")));
        }
        if (StrUtil.isNotEmpty(param.getCategory())) {
            // 根据分类获取支持的文件后缀信息
            val description = OssCategoryEnums.getDescription(param.getCategory());
            if (StrUtil.isNotEmpty(description)) {
                query.in(OssFile.FILE_EXTENSION, Arrays.asList(description.split(",")));
            }
        }
        if (StrUtil.isNotEmpty(param.getNote())) {
            query.eq(OssFile.NOTE, param.getNote());
        }
        // 排序
        if (StrUtil.isNotEmpty(queryParam.getSortBy())) {
            if (BasePageReqDTO.ORDER_DESC.equals(queryParam.getOrder())) {
                query.orderByDesc(OssFile.ID);
            } else if (BasePageReqDTO.ORDER_ASC.equals(queryParam.getOrder())) {
                query.orderByAsc(OssFile.ID);
            }
        }
        // 模糊查询
        if (StrUtil.isNotEmpty(queryParam.getKeyword())) {
            query.and(queryWrapper -> {
                queryWrapper.like(OssFile.NAME, queryParam.getKeyword()).or().like(OssFile.FILE_ID,
                        queryParam.getKeyword());
            });
        }
        // 分页
        if (queryParam.getPage()) {
            page = new Page<>(queryParam.getPageNum(), queryParam.getPageSize(), true);
        } else {
            page = new Page<>(queryParam.getPageNum(), ossFileMapper.selectCount(query), true);
        }
        val result = ossFileMapper.selectPage(page, query);

        // 对结果进行排序,按照目录排在前面,文件排在后面
        result.getRecords().sort(new OssFileListComparator());
        return result.convert(this::convertVo);
    }

    @Override
    @SneakyThrows
    public OssFileVo upload(MultipartFile file, OssUploadParam param) {
        val fileName = file.getOriginalFilename();
        val path = getFilePath(param.getBucket(), fileName);
        FileUtil.writeFromStream(file.getInputStream(), path);
        param.setPath(path);
        // 针对不同的文件生成缩略图
        if (param.isGenerateCover()) {
            val coverPath = generateCover(path);
            param.setCover(coverPath);
        }
        val ossFile = saveOssFile(param);
        return convertVo(ossFile);
    }

    @Override
    public boolean uploadChunk(MultipartFile file, OssUploadChunkParam param) {
        // 构造文件块的保存路径
        val fileId = param.getFileId();
        var ossFile = getByFileIdFromDb(fileId);
        if (ossFile == null) {
            ossFile = new OssFile();
            ossFile.setFileId(fileId);
        } else {
            // 校验是否已经上传完成
            if (Objects.equals(ossFile.getTotalChunk(), ossFile.getChunk())) {
                Asserts.fail(OssErrorTips.FILE_ALREADY_EXIST);
            }
        }
        // 直接以分块名称最为文件名称
        val chunkFileName = param.getChunkNumber() + CHUNK_SUFFIX;
        // log.info("chunkFileName=" + chunkFileName + ",total=" +
        // param.getTotalChunkNumber());
        val uploadFolder = Paths.get(pathUtils.getBasePath(), param.getBucket(), fileId).toString();
        FileUtil.mkdir(uploadFolder);
        Path chunkPath = Paths.get(uploadFolder, chunkFileName);
        try {
            FileUtil.writeFromStream(file.getInputStream(), chunkPath.toString());
        } catch (IOException e) {
            Asserts.fail(OssErrorTips.SAVE_OSS_FILE_FAILED);
        }
        // 将OSS信息保存活更新到数据库当中
        ossFile.setName(param.getFileName());
        ossFile.setChunk(param.getChunkNumber());
        ossFile.setTotalChunk(param.getTotalChunkNumber());
        ossFile.setFileExtension(param.getSuffix());
        ossFile.setBucket(param.getBucket());
        ossFile.setPath(uploadFolder);
        //说明是无token,为了兼容mybatis修改数据的时候会设置创建人和修改人
        if (StrUtil.isEmpty(param.getBucket())) {
            ossFile.setCreateBy(0L);
            ossFile.setUpdateBy(0L);
        }
        //判断是否是压缩包文件
        // log.info("file.getOriginalFilename():{},isZip:{}", file.getOriginalFilename(), MyFileUtils.isZip(file.getOriginalFilename()));
        if ("zip".equals(param.getSuffix())) {
            // ossFile.setManufacturer("0");
        }
        // TODO 后续看合并分块逻辑需不需要在这里处理
        if (param.getChunkNumber() == param.getTotalChunkNumber()) {
            // 合并文件名称,时间戳方便排序
            val mergeFileName = fileId + "." + param.getSuffix();
            val mergePath = Paths.get(pathUtils.getBasePath(), param.getBucket(), mergeFileName);
            val startMergeTime = System.currentTimeMillis();
            val mergeFlag = mergeFileChunks(mergePath, param.getBucket(), param.getFileId());
            log.info("合并文件耗时:{}毫秒", System.currentTimeMillis() - startMergeTime);
            if (!mergeFlag) {
                Asserts.fail(OssErrorTips.MERGE_CHUNK_FILE_FAILED);
            }
            // 封面生成目前依赖于ffmpeg环境,默认不生成
            if (param.isGenerateCover()) {
                String coverPath = generateCover(mergePath.toString());
                ossFile.setCover(coverPath);
            }
            // 设置最终保存文件
            ossFile.setPath(mergePath.toString());

            //设置文件大小
            ossFile.setFileSize(FileUtil.size(mergePath.toFile()));
        }
        var saveOrUpdateFlag = false;
        try {
            saveOrUpdateFlag = saveOrUpdate(ossFile);
            if (!saveOrUpdateFlag) {
                // 保存失败则删除当前块数
                FileUtil.del(chunkPath);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return saveOrUpdateFlag;
    }

    @Override
    public GetChunkVo getChunk(OssGetChunkParam param) {
        val result = new GetChunkVo();
        // 检查下该文件ID是否已存在
        val fileName = param.getFileId() + "." + param.getSuffix();
        val file = Paths.get(pathUtils.getBasePath(), param.getBucket(), fileName).toString();
        if (FileUtil.exist(file)) {
            result.setChunk(-1);
            result.setUrl(getOssFileUrl(param.getFileId()));
            return result;
        }
        val uploadFolder = getUploadBigFileFolder(param.getBucket(), param.getFileId());
        // 读取目录里的所有文件,根据文件数量来判断当前上传的进度,但这种方式可能存在分块跳序上传,造成遗漏
        val chunkFiles = FileUtil.loopFiles(uploadFolder.toString()).stream()
                .filter(chunkfile -> chunkfile.getName().endsWith(CHUNK_SUFFIX)).collect(Collectors.toList());
        if (CollUtil.isEmpty(chunkFiles)) {
            result.setChunk(0);
            return result;
        }
        int chunk = chunkFiles.size();
        for (int i = 1; i < chunkFiles.size(); i++) {
            // 从1块开始查找,看看是否有缺的,如果有缺则从当前块开始传输
            val chunkFileName = i + CHUNK_SUFFIX;
            val chunkFile = Paths.get(uploadFolder.toString(), chunkFileName).toFile();
            if (!chunkFile.exists() || chunkFile.length() == 0) {
                chunk = i;
                break;
            }
        }
        result.setChunk(chunk);
        return result;
    }

    // 逻辑基本和upload方法一样,
    @Override
    public OssFileVo uploadByBase64(OssUploadBase64Param param) {
        if (StrUtil.isEmpty(param.getSuffix())) {
            param.setSuffix("jpg");
        }
        val fileName = System.currentTimeMillis() + "." + param.getSuffix();
        val path = getFilePath(param.getBucket(), fileName);

        if (StrUtil.isEmpty(param.getBase64Data())) {
            Asserts.fail(OssErrorTips.FILE_NOT_EXIST);
        }
        byte[] decodedData = null;
        if (param.getBase64Data().contains(",")) {
            decodedData = Base64.getDecoder().decode(param.getBase64Data().split(",")[1]);
        } else {
            decodedData = Base64.getDecoder().decode(param.getBase64Data());
        }
        // byte[] decodedData = Base64.getDecoder().decode(param.getBase64Data());
        FileUtil.writeBytes(decodedData, path);
        val ossFile = saveOssFile(path, param);
        return convertVo(ossFile);
    }

    /**
     * 将分块文件进行合并
     *
     * @param mergedFilePath 合成后文件路径
     */
    @SneakyThrows
    @Override
    public boolean mergeFileChunks(Path mergedFilePath, String bucket, String fileId) {
        val uploadDir = Paths.get(pathUtils.getBasePath(), bucket, fileId).toString();
        // 合并文件块最终文件地址
        FileUtil.del(mergedFilePath);
        val childFiles = FileUtil.loopFiles(uploadDir).stream()
                .filter(chunkfile -> chunkfile.getName().endsWith(CHUNK_SUFFIX)).collect(Collectors.toList());
        log.info("分块大小:" + childFiles.size());
        if (CollUtil.isEmpty(childFiles)) {
            Asserts.fail(OssErrorTips.MERGE_CHUNK_FILE_ERROR);
            return false;
        }
        childFiles.sort(new ChunkFileComparator());
        for (File chunkFile : childFiles) {
            if (chunkFile.exists()) {
                Files.write(mergedFilePath, Files.readAllBytes(Paths.get(chunkFile.getAbsolutePath())),
                        StandardOpenOption.CREATE, StandardOpenOption.APPEND);
            } else {
                Asserts.fail(OssErrorTips.MERGE_CHUNK_FILE_ERROR);
                break;
            }
        }
        // 合并成功后删除碎片
        FileUtil.del(uploadDir);
        return true;
    }

    @Override
    public OssFileVo createDir(OssCreateDirParam param) {
        val ossFile = new OssFile();
        ossFile.setFileId(generateFileId());
        ossFile.setName(param.getName());
        ossFile.setDir(true);
        ossFile.setParentFileId(param.getParentFileId());
        val flag = save(ossFile);
        return flag ? convertVo(ossFile) : null;
    }

    @Override
    public OssFileVo updateDir(String fileId, OssUpdateDirParam param) {
        val ossFile = getByFileIdFromDb(fileId);
        if (ossFile == null) {
            Asserts.fail(OssErrorTips.FILE_ID_NOT_EXIST);
        }
        ossFile.setName(param.getName());
        val updateDirFlag = updateById(ossFile);
        return updateDirFlag ? convertVo(ossFile) : null;
    }

    @Override
    @Transactional
    public boolean moveFiles2Dir(OssFileMove2DirParam param) {
        val fileIdList = Arrays.asList(param.getFileIds().split(","));
        val ossFileList = listByFileIds(fileIdList);
        if (CollUtil.isEmpty(ossFileList)) { // 如果没有找到文件,则直接返回
            return true;
        }
        // 调整其父目录ID
        ossFileList.forEach(ossFile -> {
            ossFile.setParentFileId(param.getParentFileId());
        });
        return updateBatchById(ossFileList);
    }

    @Override
    public boolean updateByFileId(String fileId, OssUpdateParam param) {
        val ossFile = getByFileIdFromDb(fileId);
        if (ossFile == null) {
            Asserts.fail(OssErrorTips.FILE_ID_NOT_EXIST);
        }
        ossFile.setName(param.getName());
        // ossFile.setCover(param.getCover());
        return updateById(ossFile);
    }

    @Override
    @SneakyThrows
    public boolean updateCover(String fileid, MultipartFile file, OssUpdateCoverParam param) {
        val ossFile = getByFileIdFromDb(fileid);
        if (ossFile == null) {
            Asserts.fail(OssErrorTips.FILE_ID_NOT_EXIST);
        }
        val dstPath = getFilePath(param.getBucket(), file.getOriginalFilename());
        FileUtil.writeFromStream(file.getInputStream(), dstPath);
        ossFile.setCover(dstPath);
        return updateById(ossFile);
    }

    @Override
    public OssFileVo getByFileId(String fileid) {
        if (StrUtil.isEmpty(fileid)) {
            return null;
        }
        val ossFile = getByFileIdFromDb(fileid);
        return convertVo(ossFile);
    }

    @Override
    public String getOssFileUrl(String fileid) {
        val ossFile = getByFileId(fileid);
        if (ossFile == null) {
            return "";
        }
        return ossFile.getUrl();
    }

    @Override
    public OssFile getByFileIdFromDb(String fileid) {
        return baseMapper.selectOne(new QueryWrapper<OssFile>().eq(OssFile.FILE_ID, fileid));
    }

    public OssFile getByFileMd5AndBucketFromDb(String fileMd5, String bucket) {
        return baseMapper
                .selectOne(new QueryWrapper<OssFile>().eq(OssFile.FILE_MD5, fileMd5).eq(OssFile.BUCKET, bucket));
    }

    @Override
    public boolean deleteByFileId(String fileid) {
        val ossFile = getByFileIdFromDb(fileid);
        if (ossFile == null) {
            return false;
        }
        val path = ossFile.getPath();
        val result = baseMapper.delete(new QueryWrapper<OssFile>().eq(OssFile.FILE_ID, fileid));
        if (result > 0) {
            FileUtil.del(path);
            // 如果这个文件是一个zip文件,删除他对应的解压目录
            if (StrUtil.isNotEmpty(path)) {
                val fileZipFolder = Paths.get(FileUtil.getParent(path, 1), FileUtil.getPrefix(FileUtil.getName(path)));
                FileUtil.del(fileZipFolder);
            }
        }
        return result > 0;
    }

    @Override
    public OssFile saveOssFile(String path) {
        val fileId = generateFileId();
        val ossFile = buildOssFile(fileId, FileUtil.getName(path), path);
        val saveFlag = save(ossFile);
        return saveFlag ? ossFile : null;
    }

    public OssFile saveOssFile(String path, OssUploadBase64Param param) {
        val fileId = generateFileId();
        val ossFile = buildOssFile(fileId, FileUtil.getName(path), path);
        ossFile.setNote(param.getClassInfo());
        if (StrUtil.isEmpty(param.getBucket())) {
            ossFile.setCreateBy(0L);
            ossFile.setUpdateBy(0L);
        }
        if (param.getCreateBy() != null) {
            ossFile.setCreateBy(param.getCreateBy());
            ossFile.setUpdateBy(param.getCreateBy());
        }
        val saveFlag = save(ossFile);
        return saveFlag ? ossFile : null;
    }

    public OssFile saveOssFile(OssUploadParam param) {
        val fileId = generateFileId();
        val path = param.getPath();
        val ossFile = buildOssFile(fileId, FileUtil.getName(path), path);
        ossFile.setCover(param.getCover());
        ossFile.setDir(false);
        ossFile.setParentFileId(param.getParentFileId());
        if (StrUtil.isEmpty(param.getBucket())) {
            ossFile.setCreateBy(0L);
            ossFile.setUpdateBy(0L);
        }
        val saveFlag = save(ossFile);
        return saveFlag ? ossFile : null;
    }

    @Override
    public boolean saveBatchOssFiles(List<String> paths) {
        if (CollUtil.isEmpty(paths)) {
            return false;
        }
        val ossFileList = new ArrayList<OssFile>();
        for (String path : paths) {
            val fileId = generateFileId();
            val ossFile = buildOssFile(fileId, FileUtil.getName(path), path);
            ossFileList.add(ossFile);
        }
        if (CollUtil.isNotEmpty(ossFileList)) {
            return saveBatch(ossFileList);
        }
        return false;
    }

    @Override
    public OssFile buildOssFile(String fileId, String fileName, String path) {
        OssFile ossFile = new OssFile();
        ossFile.setFileId(fileId);
        ossFile.setFileExtension(FileUtil.getSuffix(fileName));
        ossFile.setName(FileUtil.getPrefix(fileName));
        ossFile.setPath(path);
        ossFile.setFileSize(FileUtil.size(FileUtil.file(path)));
        val isDir = FileUtil.isDirectory(path);
        ossFile.setDir(isDir);
        return ossFile;
    }

    @SneakyThrows
    @Override
    public OssFileVo convertVo(OssFile ossFile) {
        if (ossFile == null) {
            return null;
        }
        val vo = new OssFileVo();
        BeanUtil.copyProperties(ossFile, vo);
        val url = pathUtils.convertNetUrl(ossFile.getPath());
        // String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
        // 编码空格
        String encodedUrl = url.replace(" ", "%20");
        vo.setUrl(encodedUrl);

        // 设置封面图片
        if (StrUtil.isNotEmpty(ossFile.getCover())) {
            // val coverUrl = getOssFileUrl(ossFile.getCover());
            vo.setCoverUrl(pathUtils.convertNetUrl(ossFile.getCover()));
        }
        return vo;
    }

    @Override
    public List<OssFileVo> convertVo(List<OssFile> ossFileList) {
        if (CollUtil.isEmpty(ossFileList)) {
            return new ArrayList<>();
        }
        return ossFileList.stream().map(this::convertVo).toList();
    }

    @Override
    public List<OssFile> listByFileExtension(String fileExtension) {
        return baseMapper.selectList(new QueryWrapper<OssFile>().eq(OssFile.FILE_EXTENSION, fileExtension));
    }

    @Override
    public List<OssFile> listByParentFileId(String parentFileId) {
        return baseMapper.selectList(new QueryWrapper<OssFile>().eq(OssFile.PARENT_FILE_ID, parentFileId));
    }

    @Override
    public String getFilePath(String bucket, String fileName) {
        val suffix = FileUtil.getSuffix(fileName);
        val newFileName = System.currentTimeMillis() + "." + suffix;
        Path basePath = Paths.get(pathUtils.getBasePath(), bucket);
        return basePath.resolve(newFileName).toString();
    }

    public String generateFileId() {
        // 后面根据业务可定制fileid
        return String.valueOf(snowflakeIdGenerator.generateId());
    }

    private Path getUploadBigFileFolder(String bucket, String fileId) {
        return Paths.get(pathUtils.getBasePath(), bucket, fileId);
    }

    /**
     * 生成封面图片
     *
     * @param srcPath 源文件路径
     * @return 文件对象
     */
    private String generateCover(String srcPath) {

        if (!FileUtil.exist(srcPath)) {
            return null;
        }
        String coverPath;
        val fileName = FileUtil.getName(srcPath);
        val newFileName = FileUtil.getPrefix(srcPath) + "_cover.jpg";
        coverPath = Paths.get(FileUtil.getParent(srcPath, 1), newFileName).toString();
        // 先判断是否是图片文件
        if (MyFileUtils.isImage(fileName)) {
            try {
                // 保持宽高比缩放
                Thumbnails.of(FileUtil.file(srcPath))
                        .scale(0.5)
                        .outputQuality(0.8f)
                        // .size(convertWidth, convertHeight)
                        .toFile(FileUtil.file(coverPath));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return coverPath;
        }
        // 视频格式
        else if (MyFileUtils.isVideo(fileName)) {
            VideoUtils.generateCover(srcPath, coverPath);
            return coverPath;
        }
        return null;

    }

    /**
     * 找出符合条件的文件,批量保存到数据库中
     *
     * @param files 文件集合
     */
    @Override
    public void filterFilesAndSaveBatch(List<File> files) {
        // 目前只保存图片、视频文件信息
        // 创建一个新的集合
        val suffixList = new ArrayList<String>();
        // 合并集合
        suffixList.addAll(MyFileUtils.IMAGE_SUFFIX);
        suffixList.addAll(MyFileUtils.VIDEO_SUFFIX);
        suffixList.addAll(MyFileUtils.MODEL_SUFFIX);
        suffixList.addAll(MyFileUtils.AUDIO_SUFFIX);
        val filterFiles = MyFileUtils.filterFilesBySuffixList(files, suffixList);
        val ossFileList = new ArrayList<OssFile>();
        filterFiles.forEach(file -> {
            val fileId = generateFileId();
            val ossFile = buildOssFile(fileId, file.getName(), file.getAbsolutePath());
            // ossFile.setCompanyId(MemberUtils.getCurrentCompanyId());
            ossFileList.add(ossFile);
        });
        if (CollUtil.isNotEmpty(ossFileList)) {
            saveBatch(ossFileList);
        }
    }

    @Override
    public List<OssFile> listByFileIds(List<String> fileIdList) {
        if (CollUtil.isEmpty(fileIdList)) {
            return new ArrayList<>();
        }
        return baseMapper.selectList(new QueryWrapper<OssFile>().in(OssFile.FILE_ID, fileIdList));
        // return
        // ossFileList.stream().map(this::convertVo).collect(Collectors.toList());
    }

}
